using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace UKVSTS.SimpleImageEditControl
{
    public partial class SimpleImageEditSurface : UserControl
    {

        private Image m_Image;

        private Single m_ScallingFactor;

        public const int MAX_ZOOM = 16;
        
        public const int MIN_ZOOM = 1;
        
        private int m_ZoomFactor = MIN_ZOOM;

        private Rectangle m_Crop = Rectangle.Empty;

        public delegate void ZoomChangedDelegate(Object sender, int newLevel);

        public event ZoomChangedDelegate ZoomChanged;

        public enum Mode {
            DrawingRectangles,
            Scrolling,
            Cropping,
            FreeHandHighlighter,
            BoxHighlighter,
            Magnify
        }
        
        private Mode m_Mode;

        class DrawnRectangle {
            public Color Color;
            public Rectangle Coordinates;

            public DrawnRectangle(Color c, Rectangle r)
            {
                Color = c;
                Coordinates = r;
            }
        }

        private Rectangle m_TempDrawingRect;

        private List<DrawnRectangle> m_DrawnRectangles = new List<DrawnRectangle>();

        private List<Point[]> m_Highlights = new List<Point[]>();


        private Size m_ScrolledOffset;

        /// <summary>
        /// Flag to indicate that the drawing surface has been resized and
        /// that we need to re-calculate the scaling factor.
        /// </summary>
        private bool m_InvalidScale;

        private Point m_MouseDown;

        private bool IsDrawing;

        private List<Point> m_TempHighlightPoints;

        public const int HIGHLIGHTERPEN_SIZE = 15;

        private readonly Color m_HighlighterColor;

        private readonly Pen m_HighlighterPen;

        private readonly SolidBrush m_CroppedOutBrush;

        private readonly Pen m_CroppedBorderPen;

        private readonly Cursor m_CropCursor;

        private readonly Cursor m_MagnifyCursor;


        public SimpleImageEditSurface()
        {
            InitializeComponent();

            m_HighlighterColor = Color.FromArgb(75, 255, 255, 0);
            m_HighlighterPen = new Pen(m_HighlighterColor, HIGHLIGHTERPEN_SIZE);

            m_CroppedOutBrush = new SolidBrush(Color.FromArgb(100, 100, 100, 100));

            m_CroppedBorderPen = Pens.Yellow;

            m_CropCursor = new Cursor(this.GetType(), "CropCursor.cur");
            m_MagnifyCursor = new Cursor(this.GetType(), "Magnify.cur");
                    
        }

        public void ZoomOnPoint(int magnification, Point pt)
        {
            // Offset the image so the the passed pt would be in the center of the screen.
            // Now when we zoom in, the ZoomFactor property will ensure that this point
            // stays in the center of the screen.
            m_ScrolledOffset.Width = m_ScrolledOffset.Width - (pt.X - (ClientRectangle.Width / 2));
            m_ScrolledOffset.Height = m_ScrolledOffset.Height - (pt.Y - (ClientRectangle.Height / 2));

            ZoomFactor = magnification;
        }

        protected override void OnMouseClick(MouseEventArgs e)
        {
            base.OnMouseClick(e);

            if (CurrentMode == Mode.Magnify) {
                // Left to zoom in, right to zoom out
                //
                if (e.Button == MouseButtons.Left)
                {
                    if (ZoomFactor < MAX_ZOOM) {
                        ZoomOnPoint(ZoomFactor * 2, e.Location);
                    }
                } else if (e.Button == MouseButtons.Right) {
                    if (ZoomFactor > MIN_ZOOM)
                    {
                        ZoomOnPoint(ZoomFactor / 2, e.Location);
                    }
                }
            }
            else if (CurrentMode == Mode.Cropping)
            {
                // If we are in cropping mode, any click on the control should just remove the 
                // current crop
                //
                m_Crop = Rectangle.Empty;
                Invalidate();
            }
        }


        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);

            Capture = true;
            m_MouseDown = e.Location;
            IsDrawing = true;

            if (CurrentMode == Mode.FreeHandHighlighter)
            {
                m_TempHighlightPoints = new List<Point>();
            }
            else if (CurrentMode == Mode.Scrolling)
            {
                // Allow for any offset we already have.
                //
                m_MouseDown.X -= m_ScrolledOffset.Width;
                m_MouseDown.Y -= m_ScrolledOffset.Height;
            }
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);

            if (IsDrawing)
            {
                if (CurrentMode == Mode.DrawingRectangles)
                {
                    m_DrawnRectangles.Add(new DrawnRectangle(Color.Red, ScreenToImage(m_TempDrawingRect)));
                }
                else if (CurrentMode == Mode.Cropping)
                {
                    m_Crop = ScreenToImage(m_TempDrawingRect);
                }
                else if (CurrentMode == Mode.FreeHandHighlighter)
                {

                    if (m_TempHighlightPoints.Count > 1)
                    {
                        m_Highlights.Add(ScreenToImage(m_TempHighlightPoints.ToArray()));
                    }
                }
            }

            IsDrawing = false;
            Capture = false;

            m_TempDrawingRect = Rectangle.Empty;
            m_TempHighlightPoints = null;
            m_MouseDown = Point.Empty;

            Invalidate();
        }

        private Rectangle ScreenToImage(Rectangle screen)
        {
            Single scale = 1.0F / ScallingFactor;
            scale /= ZoomFactor;

            return new Rectangle((int)((screen.X - m_ScrolledOffset.Width) * scale),
                                (int)((screen.Y - m_ScrolledOffset.Height) * scale),
                                (int)(screen.Width * scale),
                                (int)(screen.Height * scale));
        }

        private Point[] ScreenToImage(Point[] screen)
        {
            Single scale = 1.0F / ScallingFactor;
            scale /= ZoomFactor;

            for (int i = 0; i < screen.Length; i++ )
            {
                screen[i].X = (int)((screen[i].X - m_ScrolledOffset.Width) * scale);
                screen[i].Y = (int)((screen[i].Y - m_ScrolledOffset.Height) * scale);
            }

            return screen;
        }

        private Size ImageSizeInScreenUnits {
            get
            {
                Single scale = 1.0F / ScallingFactor;
                scale /= ZoomFactor;
                return new Size((int)(m_Image.Width / scale), (int)(m_Image.Height / scale));
            }
        }

        private void AdjustOffsetToRemainOnScreen()
        {
            Size imgSize = ImageSizeInScreenUnits;
            Rectangle clientRect = ClientRectangle;

            if (m_ScrolledOffset.Width > 0)
            {
                m_ScrolledOffset.Width = 0;
            }
            if (m_ScrolledOffset.Height > 0)
            {
                m_ScrolledOffset.Height = 0;
            }


            // Ensure that the offset does not put the bottom or the right edges
            // inside the client rect.
            if (clientRect.Width - m_ScrolledOffset.Width > imgSize.Width)
            {
                m_ScrolledOffset.Width = clientRect.Width - imgSize.Width;
            }
            if (clientRect.Height - m_ScrolledOffset.Height > imgSize.Height)
            {
                m_ScrolledOffset.Height = clientRect.Height - imgSize.Height;
            }

            // If the image is now smaller than the screen, remove any offset
            // Do this last so other boundy conditions will not overwrite this.
            //
            if (imgSize.Width < clientRect.Width)
            {
                m_ScrolledOffset.Width = 0;
            }
            if (imgSize.Height < clientRect.Height)
            {
                m_ScrolledOffset.Height = 0;
            }
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);

            if (IsDrawing)
            {
                Size imgSz = ImageSizeInScreenUnits;
                
                if (CurrentMode == Mode.DrawingRectangles || CurrentMode == Mode.Cropping)
                {
                    Point constrainedMouse = new Point (e.X, e.Y);

                    if (constrainedMouse.X < 0) { constrainedMouse.X = 0; }
                    if (constrainedMouse.X > imgSz.Width) { constrainedMouse.X = imgSz.Width; }

                    if (constrainedMouse.Y < 0) { constrainedMouse.Y = 0; }
                    if (constrainedMouse.Y > imgSz.Height) { constrainedMouse.Y = imgSz.Height; }


                    m_TempDrawingRect = new Rectangle(Math.Min(m_MouseDown.X, constrainedMouse.X),
                                                      Math.Min(m_MouseDown.Y, constrainedMouse.Y),
                                                      Math.Abs(m_MouseDown.X - constrainedMouse.X),
                                                      Math.Abs(m_MouseDown.Y - constrainedMouse.Y));

                }
                else if (CurrentMode == Mode.FreeHandHighlighter)
                {
                    m_TempHighlightPoints.Add (new Point (e.X, e.Y));
                }
                else if (CurrentMode == Mode.Scrolling)
                {
                    m_ScrolledOffset = new Size (e.Location.X - m_MouseDown.X,
                                                 e.Location.Y - m_MouseDown.Y);

                    AdjustOffsetToRemainOnScreen();
                }
                Invalidate();
            }
        }

        public Mode CurrentMode
        {
            get { return m_Mode; }
            set
            {
                switch (value)
                {
                    case Mode.DrawingRectangles:
                        Cursor = Cursors.Cross;
                        break;

                    case Mode.Scrolling:
                        Cursor = Cursors.Hand;
                        break;

                    case Mode.Cropping:
                        Cursor = m_CropCursor;
                        break;

                    case Mode.FreeHandHighlighter:
                        Cursor = Cursors.IBeam;
                        break;

                    case Mode.Magnify:
                        Cursor = m_MagnifyCursor;
                        break;

                    default:
                        throw new ArgumentException("Mode", "Invalid mode value");
                }

                m_Mode = value;
            }
        }

        private Size ScrollOffset 
        {
            get { return m_ScrolledOffset; }

            set
            {
                if (value.Width > 0)
                {
                    value.Width = 0;
                }
                if (value.Height > 0)
                {
                    value.Height = 0;
                }

                Size imgSz = ImageSizeInScreenUnits;
                if (ClientRectangle.Width - value.Width > imgSz.Width)
                {
                    value.Width = ClientRectangle.Width - imgSz.Width;
                }
                if (ClientRectangle.Height - value.Height > imgSz.Height)
                {
                    value.Height = ClientRectangle.Height - imgSz.Height;
                }

                m_ScrolledOffset = value;
            }
        }

        public Single ScallingFactor
        {
            get { return m_ScallingFactor; }
        }

        public int ZoomFactor
        {
            get { return m_ZoomFactor; }
            set
            {
                if (value > MAX_ZOOM || value < MIN_ZOOM)
                {
                    throw new ArgumentOutOfRangeException("ZoomFactor");
                }

                if (value != m_ZoomFactor)
                {
                    Single zoomingBy = (Single)value / (Single)m_ZoomFactor;
                    m_ZoomFactor = value;

                    if (m_Image != null)
                    {
                        // Update our scrolled offset.
                        // This calculation puts what ever was in the top left hand corner back
                        // into the top left had corner of our smaller / enlarged view.
                        m_ScrolledOffset.Width = (int)(m_ScrolledOffset.Width * zoomingBy);
                        m_ScrolledOffset.Height = (int)(m_ScrolledOffset.Height * zoomingBy);

                        // Now adjust the offset to put the what was in the center, back
                        // in the center.
                        //
                        Size centeringOffset = new Size ();
                        centeringOffset.Width = (int)(((ClientRectangle.Width - (ClientRectangle.Width / zoomingBy)) / 2) * zoomingBy);
                        centeringOffset.Height = (int)(((ClientRectangle.Height - (ClientRectangle.Height / zoomingBy)) / 2) * zoomingBy);

                        if (zoomingBy > 0)
                        {
                            // zooming in
                            //
                            m_ScrolledOffset -= centeringOffset;
                        }
                        else
                        {
                            // zooming out
                            m_ScrolledOffset += centeringOffset;

                        }

                        // Ensure that we haven't just scrolled the image outside of our viewing area.
                        //
                        AdjustOffsetToRemainOnScreen();

                        Invalidate();

                        if (ZoomChanged != null)
                        {
                            ZoomChanged(this, value);
                        }
                    }
                }
            }
        }

        public Rectangle Crop
        {
            get { return m_Crop; }
            set { 
                m_Crop = value;
                Invalidate();
            }
        }

        public Image Image
        {
            get { return m_Image; }
            set
            {
                m_Image = value;
                RecalculateScale();
                Invalidate();
            }
        }

        private void RecalculateScale () {
            // To calc the scale correctly, we need the forms grfx object to access the current DPI.
            // So as we can't do the calc here, lets just set a flag to say we are going to need
            // to re-calc on the next redraw.
            //
            m_InvalidScale = true;
        }

        public Image GetModifiedImage()
        {
            Bitmap bmp = new Bitmap(m_Image, (m_Crop == Rectangle.Empty) ? m_Image.Size : m_Crop.Size);
            using (Graphics grfx = Graphics.FromImage(bmp))
            {
                grfx.TranslateTransform(-m_Crop.Left, -m_Crop.Top);
                DrawImageAndMarkings(grfx);
            }

            return bmp;
        }

        private void DrawImageAndMarkings(Graphics grfx)
        {
            grfx.DrawImageUnscaledAndClipped(m_Image, new Rectangle(0, 0, m_Image.Width, m_Image.Height));

            foreach (DrawnRectangle r in m_DrawnRectangles)
            {
                grfx.DrawRectangle(new Pen(r.Color), r.Coordinates);
            }

            foreach (Point[] points in m_Highlights)
            {
                grfx.DrawLines(m_HighlighterPen, points);
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (m_Image != null)
            {
                // Do we need to re-calculate the scaling factor.
                // This normally occures as a result of a resize.
                //
                if (m_InvalidScale)
                {
                    Size ImageSize = m_Image.Size;

                    // calculate the image dimensions in pixels from its logical size
                    int cx = (int)((float)ImageSize.Width / m_Image.HorizontalResolution * e.Graphics.DpiX);
                    int cy = (int)((float)ImageSize.Height / m_Image.VerticalResolution * e.Graphics.DpiY);

                    Single sx = (Single)this.ClientRectangle.Width / cx;
                    Single sy = (Single)this.ClientRectangle.Height / cy;

                    m_ScallingFactor = Math.Min(sx, sy);

                    m_InvalidScale = false;
                }

                Single scaleTransform = ScallingFactor * ZoomFactor;

                e.Graphics.TranslateTransform(m_ScrolledOffset.Width, m_ScrolledOffset.Height);
                e.Graphics.ScaleTransform(scaleTransform, scaleTransform);

                DrawImageAndMarkings(e.Graphics);


                if (m_Crop != Rectangle.Empty)
                {
                    Region greyedOut = new Region (new Rectangle (0, 0, m_Image.Width, m_Image.Height));
                    greyedOut.Exclude (m_Crop);

                    e.Graphics.FillRegion(m_CroppedOutBrush, greyedOut);

                    e.Graphics.DrawRectangle(m_CroppedBorderPen, m_Crop);
                }


                if (m_TempHighlightPoints != null && m_TempHighlightPoints.Count > 1)
                {
                    // If we just work on the screen size (no zooming) here, we can 
                    // save having to transform to image coords.
                    e.Graphics.ResetTransform();

                    Single scale = 1.0F / ScallingFactor;
                    scale /= ZoomFactor;

                    e.Graphics.DrawLines(new Pen(m_HighlighterColor, HIGHLIGHTERPEN_SIZE / scale), m_TempHighlightPoints.ToArray());
                }

                if (m_TempDrawingRect != Rectangle.Empty)
                {
                    // If we just work on the screen size (no zooming) here, we can 
                    // save having to transform to image coords.
                    e.Graphics.ResetTransform();

                    e.Graphics.DrawRectangle(Pens.Purple, m_TempDrawingRect);
                }
            }
        }

        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            if (m_Image != null)
            {
                RecalculateScale();
                AdjustOffsetToRemainOnScreen();

                Invalidate();
            }
        }

        public bool IsDirty
        {
            get
            {
                // Have we cropped, drawn rectangles or highlights?
                //
                return (m_Crop != null && m_Crop != Rectangle.Empty) ||
                       m_DrawnRectangles.Count != 0 ||
                       m_Highlights.Count != 0;
            }
        }
    }
}
